package com.huidian.gitbatchprocessing.git.util;

import com.huidian.gitbatchprocessing.config.CredentialsConfig;
import com.huidian.gitbatchprocessing.exception.BranchCantDeleteException;
import com.huidian.gitbatchprocessing.exception.BranchNotExistException;
import com.huidian.gitbatchprocessing.exception.ProjectNotCleanException;
import com.huidian.gitbatchprocessing.git.entity.MyMergeResult;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.dvcs.repo.RepoStateException;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import org.apache.commons.collections.CollectionUtils;
import org.eclipse.jgit.api.*;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.RefNotFoundException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.*;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * git 操作工具类
 *
 * @author zxb
 * @date 2021/6/17
 */
public final class GitUtil {
    public static final String DOT_GIT = ".git";
    public static final String ORIGIN_HEAD = "origin/HEAD";
    public static final String HEAD = "HEAD";
    public static final String CHERRY_PICK_HEAD = "CHERRY_PICK_HEAD";
    public static final String MERGE_HEAD = "MERGE_HEAD";
    public static final String REBASE_HEAD = "REBASE_HEAD";
    private static final String REPO_PATH_LINK_PREFIX = "gitdir:";
    private static final String HEAD_FILE = "HEAD";
    private static final String RELEASE_BRANCH = "release";
    private static final String MASTER_BRANCH = "master";
    private final static Logger LOG = LoggerFactory.getLogger(GitUtil.class);

    private GitUtil() {
    }

    /**
     * 查询项目根路径下的 git 仓库
     *
     * @return
     */
    @NotNull
    public static List<Repository> findGitDirs(Project project) {
        // 获取当前项目所有 modules
        Module[] modules = ModuleManager.getInstance(project).getModules();
        List<Repository> results = new ArrayList<>();
        if (modules == null) {
            return results;
        }
        // 遍历 modules，获取所有的 git 仓库
        for (Module module : modules) {
            VirtualFile[] contentRoots = ModuleRootManager.getInstance(module).getContentRoots();
            for (VirtualFile virtualFile : contentRoots) {
                try {
                    Repository repository = getRepository(virtualFile);
                    if (repository != null) {
                        results.add(repository);
                    }
                } catch (IOException e) {
                    // pass
                }
            }
        }
        return results;
    }

    /**
     * 判断文件是否为 git 文件目录，并且实例化为 git 仓库
     * 1. 当前目录包含 .git
     * 2. 子目录包含   .git
     *
     * @param virtualFile
     * @return
     * @throws IOException
     */
    private static Repository getRepository(VirtualFile virtualFile) throws IOException {
        String path = virtualFile.getPath();
        // 子目录存在 .git
        if (GitUtil.isGitRoot(Paths.get(path))) {
            return new FileRepositoryBuilder()
                    .setGitDir(new File(path + File.separator + DOT_GIT))
                    .build();
        } else if (GitUtil.isGitDir(new File(path))) {
            // 当前目录存在 .git
            return new FileRepositoryBuilder()
                    .setGitDir(new File(path))
                    .build();
        }
        return null;
    }

    @Nullable
    private static File findRealRepositoryDir(@NotNull @NonNls Path rootPath, @NotNull @NonNls String path) {
        if (!FileUtil.isAbsolute(path)) {
            String canonicalPath = FileUtil.toCanonicalPath(FileUtil.join(rootPath.toString(), path), true);
            path = FileUtil.toSystemIndependentName(canonicalPath);
        }
        File file = new File(path);
        return file.isDirectory() ? file : null;
    }

    @Nullable
    private static String parsePathToRepository(@NotNull @NonNls String content) {
        content = content.trim();
        if (content.startsWith(REPO_PATH_LINK_PREFIX)) {
            content = content.substring(REPO_PATH_LINK_PREFIX.length()).trim();
        }
        if (content.isEmpty() || content.contains("\n")) {
            return null;
        }
        return content;
    }

    /**
     * 是否是 git 的根目录
     *
     * @param rootDir
     * @return
     */
    public static boolean isGitRoot(@NotNull Path rootDir) {
        Path dotGit = rootDir.resolve(DOT_GIT);
        BasicFileAttributes attributes;
        try {
            attributes = Files.readAttributes(dotGit, BasicFileAttributes.class);
        } catch (IOException ignore) {
            return false;
        }

        if (attributes.isDirectory()) {
            try {
                BasicFileAttributes headExists = Files.readAttributes(dotGit.resolve(HEAD_FILE), BasicFileAttributes.class);
                return headExists.isRegularFile();
            } catch (IOException ignore) {
                return false;
            }
        }
        if (!attributes.isRegularFile()) {
            return false;
        }

        String content;
        try {
            content = DvcsUtil.tryLoadFile(dotGit.toFile());
        } catch (RepoStateException e) {
            LOG.error(e.getMessage());
            return false;
        }

        String pathToDir = parsePathToRepository(content);
        if (pathToDir == null) {
            return false;
        }
        return findRealRepositoryDir(rootDir, pathToDir) != null;
    }

    /**
     * 是否是 git 目录
     *
     * @param file
     * @return
     */
    public static boolean isGitDir(@NotNull File file) {
        Path dir = file.toPath();
        BasicFileAttributes attributes;
        try {
            attributes = Files.readAttributes(dir, BasicFileAttributes.class);
        } catch (IOException ignore) {
            return false;
        }

        if (attributes.isDirectory()) {
            try {
                // .git 文件夹是否有 HEAD 文件
                BasicFileAttributes headExists = Files.readAttributes(dir.resolve(HEAD_FILE), BasicFileAttributes.class);
                return headExists.isRegularFile();
            } catch (IOException ignore) {
                return false;
            }
        }
        return false;
    }

    /**
     * 创建分支
     *
     * @param git              git 仓库
     * @param sourceBranchName 源分支
     * @param targetBranchName 目标分支
     * @throws Exception
     */
    public static void createBranch(Git git, String sourceBranchName, String targetBranchName) throws Exception {
        CredentialsProvider credentialsProvider = getCredentialsProvider();
        checkout(git, sourceBranchName);
        try {
            boolean sourceIsBranch = checkBranchNameExist(git, sourceBranchName);
            if (sourceIsBranch) {
                createBranchByBranch(git, sourceBranchName, targetBranchName, credentialsProvider);
            } else {
                createBranchByTag(git, sourceBranchName, targetBranchName, credentialsProvider);
            }
            checkout(git, targetBranchName);
        } catch (RefNotFoundException e) {
            throw new BranchNotExistException("源分支不存在，请检查您的分支，" + e.getMessage());
        }
    }

    /**
     * 切换分支
     *
     * @param git        git 仓库
     * @param branchName 需要切换到的分支名
     * @throws Exception
     */
    public static void checkout(Git git, String branchName) throws Exception {
        Status call = git.status().call();
        if (!call.isClean()) {
            throw new ProjectNotCleanException();
        }
        fetch(git);
        if (localHasBranch(git, branchName)) {
            git.checkout().setCreateBranch(false).setName(branchName).call();
            pull(git);
        } else {
            git.checkout().setCreateBranch(true).setName(branchName).setStartPoint("origin/" + branchName).call();
        }
    }

    /**
     * 撤消对文件的修改；见：https://git-scm.com/book/zh/v2/Git-%E5%9F%BA%E7%A1%80-%E6%92%A4%E6%B6%88%E6%93%8D%E4%BD%9C
     *
     * @param git
     * @param fileList
     * @throws Exception
     */
    private static void checkout(Git git, List<String> fileList) throws Exception {
        CheckoutCommand cmd = git.checkout();
        cmd.addPaths(fileList).call();
    }

    /**
     * 拉取代码
     *
     * @param git
     * @throws Exception
     */
    public static void pull(Git git) throws Exception {
        PullCommand pullCommand = git.pull().setCredentialsProvider(getCredentialsProvider());
        pullCommand.call();
    }

    /**
     * 删除分支，将同时删除本地与远程的分支
     *
     * @param git        git 仓库
     * @param branchName 需要删除的分支名
     * @throws Exception
     */
    public static void deleteBranch(Git git, String branchName) throws Exception {
        if (RELEASE_BRANCH.equals(branchName) || MASTER_BRANCH.equals(branchName)) {
            throw new BranchCantDeleteException();
        }
        if (!localHasBranch(git, branchName)) {
            throw new BranchNotExistException("该分支不存在，请检查您的分支");
        } else {
            // 切换至 release 分支
            checkout(git, MASTER_BRANCH);
            // 强制删除本地分支
            git.branchDelete().setBranchNames(joinBranName(branchName)).setForce(true).call();
            RefSpec refSpec = (new RefSpec()).setSource(null).setDestination(joinBranName(branchName));
            // 删除远程分支
            PushCommand cmd = git.push();
            cmd.setCredentialsProvider(getCredentialsProvider());
            cmd.setRefSpecs(refSpec).setRemote("origin").call();
        }
    }

    /**
     * 合并分支
     *
     * @param git              git 仓库
     * @param sourceBranchName 源分支
     * @param targetBranchName 目标分支
     * @throws Exception
     */
    public static MyMergeResult merge(Git git, String sourceBranchName, String targetBranchName) throws Exception {
        return merge(git, sourceBranchName, targetBranchName, Collections.emptyList());
    }

    /**
     * 合并分支
     *
     * @param git              git 仓库
     * @param sourceBranchName 源分支
     * @param targetBranchName 目标分支
     * @param ignoreFileList   忽略文件名集合
     * @throws Exception
     */
    public static MyMergeResult merge(Git git, String sourceBranchName, String targetBranchName, List<String> ignoreFileList) throws Exception {
        /*1. 校验*/
        // 存在分支名，则表示不是 tag
        // 原分支是否为分支
        boolean sourceIsBranch = checkBranchNameExist(git, sourceBranchName);
        if (sourceIsBranch) {
            try {
                // 切换到源分支，并执行 pull 操作
                checkout(git, sourceBranchName);
            } catch (RefNotFoundException e) {
                throw new BranchNotExistException("源分支不存在，请检查您的分支");
            }
        }
        try {
            // 切换到目标分支，并执行 pull 操作
            checkout(git, targetBranchName);
        } catch (RefNotFoundException e) {
            throw new BranchNotExistException("目标分支不存在，请检查您的分支");
        }

        if (!sourceIsBranch && !checkTagExist(git, sourceBranchName)) {
            throw new BranchNotExistException("源分支不存在，请检查您的分支");
        }

        /*2. 合并操作*/
        String msg;
        boolean success = false;
        List<String> conflictNameList = new ArrayList<>();
        Ref sourceRef = git.getRepository().findRef(sourceBranchName);
        MergeCommand mgCmd = git.merge();
        mgCmd.include(sourceRef);
        MergeResult res = mgCmd.call();
        // 存在冲突
        if (res.getMergeStatus().equals(MergeResult.MergeStatus.CONFLICTING)) {
            msg = "存在冲突，请解决后提交...";
            List<String> revertFileList = new ArrayList<>();
            List<String> conflictFileList = new ArrayList<>(res.getConflicts().keySet());
            if (CollectionUtils.isNotEmpty(ignoreFileList)) {
                conflictFileList.forEach(str -> {
                    if (ignoreFileList.contains(str)) {
                        revertFileList.add(str);
                    }
                });
            }
            if (CollectionUtils.isNotEmpty(revertFileList)) {
                LOG.info("merge 自动忽略文件 {},开始撤销更改", revertFileList);
                reset(git, revertFileList);
                checkout(git, revertFileList);
                LOG.info("merge 自动忽略文件 {},撤销更改完成", revertFileList);
            }

            if (revertFileList.size() != conflictFileList.size()) {
                conflictNameList.addAll(conflictFileList);
                success = false;
            }
        } else {
            if (!res.getMergeStatus().isSuccessful()) {
                throw new RuntimeException("merge 失败,merge 结果" + res.getMergeStatus().name());
            }
            if (res.getMergeStatus().equals(MergeResult.MergeStatus.ALREADY_UP_TO_DATE)) {
                LOG.info("merge 项目完成，未合并到新代码");
                msg = "merge 项目完成，未合并到新代码";
                success = true;
            } else {
                msg = "merge 项目完成，已合并到新代码";
                LOG.info("merge 项目完成，已合并到新代码,merge 结果:{}", res.getMergeStatus().toString());
            }
        }
        return new MyMergeResult()
                .setSuccess(success)
                .setMsg(msg)
                .setConflictNameList(conflictNameList);
    }

    private static void reset(Git git, List<String> pathList) throws GitAPIException {
        ResetCommand cmd = git.reset();
        pathList.forEach(cmd::addPath);
        cmd.call();
    }

    public static boolean statusIsClean(Git git) throws GitAPIException {
        return git.status().call().isClean();
    }

    public static String fetch(Git git) throws GitAPIException {
        FetchCommand fetchCommand = git.fetch()
                .setCredentialsProvider(getCredentialsProvider())
                .setForceUpdate(Boolean.TRUE);
        return fetchCommand.call().getMessages();
    }

    /**
     * 分支名本地是否存在
     *
     * @param git
     * @param branchName
     * @return
     * @throws GitAPIException
     */
    public static boolean localHasBranch(Git git, String branchName) throws GitAPIException {
        List<Ref> refs = git.branchList().call();
        return refs.stream().anyMatch(ref -> ref.getName().equals(joinBranName(branchName)));
    }

    /**
     * 用户认证信息
     *
     * @return
     */
    public static CredentialsProvider getCredentialsProvider() {
        CredentialsConfig config = CredentialsUtil.getCredentialsConfigFromLocal(ProjectUtil.get());
        CredentialsProvider usernamePasswordCredentialsProvider = new UsernamePasswordCredentialsProvider(config.getUserName(), config.getPassword());
        return new ChainingCredentialsProvider(new NetRCCredentialsProvider(), usernamePasswordCredentialsProvider);
    }

    /**
     * 检查标签名是否存在
     *
     * @param git     git 仓库
     * @param tagName 需要检查的标签名
     * @return
     * @throws Exception
     */
    public static boolean checkTagExist(Git git, String tagName) throws Exception {
        List<String> tagNameList = getAllTagNames(git);
        return tagNameList.contains(tagName);
    }

    /**
     * 检查分支名是否存在
     *
     * @param git        git 仓库
     * @param branchName 需要检查的分支名
     * @return
     * @throws Exception
     */
    public static boolean checkBranchNameExist(Git git, String branchName) throws Exception {
        List<String> branchNameList = getAllBranchNames(git);
        return branchNameList.contains(branchName);
    }

    /**
     * 获取所有标签
     *
     * @param git
     * @return
     * @throws Exception
     */
    public static List<String> getAllTagNames(Git git) throws Exception {
        List<Ref> tagList = git.tagList().call();
        List<String> tagNameList = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(tagList)) {
            tagList.forEach(ref -> {
                String[] names = ref.getName().split("/");
                String name = names[names.length - 1];
                tagNameList.add(name);
            });
        }
        return tagNameList;
    }

    /**
     * 获取所有分支名
     *
     * @param git git 仓库
     * @return 所有分支名
     * @throws Exception
     */
    public static List<String> getAllBranchNames(Git git) throws Exception {
        List<Ref> branchList = git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call();
        List<String> branchNameList = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(branchList)) {
            branchList.forEach(ref -> {
                String[] names = ref.getName().split("/");
                String name = names[names.length - 1];
                branchNameList.add(name);
            });
        }
        return branchNameList;
    }

    /**
     * 创建分支
     *
     * @param git
     * @param sourceBranchName
     * @param targetBranchName
     * @param credentialsProvider
     * @throws Exception
     */
    private static void createBranchByBranch(Git git, String sourceBranchName, String targetBranchName, CredentialsProvider credentialsProvider) throws Exception {
        Ref ref = git.branchCreate().setStartPoint(sourceBranchName).setName(targetBranchName).call();
        pushNewBranch(git, targetBranchName, ref, credentialsProvider);
    }

    /**
     * 创建标签
     *
     * @param git
     * @param sourceTagName
     * @param branchName
     * @param credentialsProvider
     * @throws Exception
     */
    public static void createBranchByTag(Git git, String sourceTagName, String branchName, CredentialsProvider credentialsProvider) throws Exception {
        Ref ref = git.branchCreate().setName(branchName).setStartPoint("refs/tags/" + sourceTagName).call();
        pushNewBranch(git, branchName, ref, credentialsProvider);
    }

    /**
     * 推送分支到远程仓库
     *
     * @param git                 git 仓库
     * @param branchName          分支名
     * @param ref                 git 的某个分支引用
     * @param credentialsProvider 认证信息
     * @throws GitAPIException
     */
    private static void pushNewBranch(Git git, String branchName, Ref ref, CredentialsProvider credentialsProvider) throws GitAPIException {
        PushCommand cmd = git.push();
        if (credentialsProvider != null) {
            cmd.setCredentialsProvider(credentialsProvider);
        }
        cmd.add(ref).call();
        git.branchCreate()
                .setName(branchName)
                .setForce(true)
                .setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.SET_UPSTREAM)
                .setStartPoint("origin/" + branchName)
                .call();
    }

    /**
     * 获取本地分支得到的结果会有 refs/heads，比如 develop 分支，查询本地的结果名为；refs/heads.develop
     *
     * @param branchName
     * @return
     */
    private static String joinBranName(String branchName) {
        return String.format("refs/heads/%s", branchName);
    }
}
